﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using System.IO;

using Sonic.Framework.Site;
using Sonic.Framework.Tools;
using Sonic.Framework.Extensions;
using Enums = Sonic.Framework.Site.Enums;
using Sonic.Framework.Interfaces;
using System.Reflection;
using Sonic.Framework.ProcessorToolDirectives;

namespace Sonic.Framework
{
    public static class SonicProcessorFactory
    {
        #region Constants

        // Allow delimited list of file patterns for a directory
        private const char FILE_PATTERN_DELIMITER = '|';

        #region Default configuration values

        // Default value for deleteSourceFiles on a Concatenation
        private const bool DEFAULT_CONCAT_DELETESOURCEFILES = false;

        // Defualt value for a output filename on a Concatenation
        private const string DEFAULT_CONCAT_OUTPUT = "~/";

        // Default value for an exclusion type
        private const Enums.ExclusionType DEFAULT_EXCLUSION_TYPE = Enums.ExclusionType.All;

        // Default value for a directory file extension pattern
        private static string[] DEFAULT_DIRECTORY_FILEPATTERN = new string[1] {"*.htm*|*.css|*.js"};

        // Default value for a diretory's relative path
        private const string DEFAULT_DIRECTORY_RELATIVEPATH = "/";

        private const Enums.SitePathScope DEFAULT_PATH_SCOPE = Enums.SitePathScope.File;

        #endregion

        #endregion

        #region Factory SonicProcessor builders
        /// <summary>
        /// Factory method to create a sonic processor from a valid XML config file
        /// </summary>
        /// <param name="filename"></param>
        /// <returns></returns>
        public static SonicProcessor GetSonicProcessor(string filename)
        {
            XDocument configXML = XDocument.Load(RelativizePath(filename));

            SonicProcessor processor = GetSonicProcessor(configXML);

            return processor;
        }

        /// <summary>
        /// Factory method to create a sonic processor from a valid config XDocument
        /// </summary>
        /// <param name="configXML">Configuration XML to load from</param>
        /// <returns></returns>
        public static SonicProcessor GetSonicProcessor(XDocument configXML)
        {
            SonicProcessor processor = new SonicProcessor();

            processor.ProcessorTools = LoadProcessorTools(configXML);
            processor.ProcessorToolDirectives = LoadProcessorToolDirectives(configXML);
            processor.Sites = LoadSites(configXML, processor);

            return processor;
        }
        #endregion

        #region Loaders

        /// <summary>
        /// Load a list of processor tools from the Xml configuration file
        /// </summary>
        /// <param name="configXML">Configuration file to load from</param>
        /// <returns>List of ProcessorTool's defined within the XML file</returns>
        private static Dictionary<string, IProcessorTool> LoadProcessorTools(XDocument configXML)
        {
            var tools = from processorTool in configXML.Element("Sonic").Element("ProcessorTools").Elements("Tool")
                        select new ProcessorTool
                        {
                            Name = processorTool.Attribute("name").Value,
                            ExecutablePath = new FileInfo(RelativizePath(processorTool.Attribute("path").Value)),
                            Arguments = processorTool.Element("Arguments").Value
                        };

            Dictionary<string, IProcessorTool> dicTools = new Dictionary<string, IProcessorTool>();
            foreach (ProcessorTool tool in tools)
            {
                if (!dicTools.ContainsKey(tool.Name))
                {
                    dicTools.Add(tool.Name, tool);
                }
            }

            // Add the null processor tool (which doesn't do anything, but provides hooks for ProcessorToolDirectives)
            dicTools.Add("Null", new ProcessorTool("Null"));

            return dicTools;
        }

        /// <summary>
        /// Load the ProcessorToolDirective plugins from the .DLL files listed in the config XML.
        /// The loaded plugins are instantiated and added to the returned dictionary.
        /// </summary>
        /// <param name="configXML"></param>
        /// <returns></returns>
        private static Dictionary<string, IProcessorToolDirective> LoadProcessorToolDirectives(XDocument configXML)
        {
            // Get a list of all plugins that need to be loaded
            var directiveDefinitions = from d in configXML.Element("Sonic").Element("ProcessorToolDirectives").Elements("Directive")
                                         select new
                                         {
                                             Name = d.Attribute("name").Value,
                                             Type = d.Attribute("type").Value,
                                             Path = d.Attribute("path").Value
                                         };

            Dictionary<string, IProcessorToolDirective> dicDirectives = new Dictionary<string, IProcessorToolDirective>();

            // Create an instance of each plugin
            foreach (var d in directiveDefinitions)
            {
                Type objType = null;
                
                try
                {
                    /* tehehe */
                    Assembly ass = null;
                    ass = Assembly.LoadFrom(d.Path);
                    
                    if (ass != null)
                    {
                        objType = ass.GetType(d.Type);
                    }
                }
                catch (FileNotFoundException)
                {
                    // Plugin DLL was not found
                }
                catch (Exception)
                {
                    // Other error
                    continue;
                }

                try
                {
                    // OK Lets create the object as we have the Type
                    if (objType != null && !objType.IsAbstract && objType.BaseType == typeof(IProcessorToolDirective))
                    {
                        IProcessorToolDirective plugin = (IProcessorToolDirective)Activator.CreateInstance(objType);
                        //plugin.Host = this;

                        if (!dicDirectives.ContainsKey(d.Name))
                        {
                            dicDirectives.Add(d.Name, plugin);
                        }
                    }
                }
                catch (Exception)
                {
                    // Other error
                    continue;
                }
            }

            return dicDirectives;
        }

        /// <summary>
        /// Load full site definitions from the XML configuration file
        /// </summary>
        /// <param name="configXML">Configuration file to load from</param>
        /// <returns>List of SiteProfile's defined within the XML file</returns>
        private static List<SiteProfile> LoadSites(XDocument configXML, SonicProcessor processor)
        {
            // The collection of site profiles defined in the config xml
            List<SiteProfile> siteProfiles = new List<SiteProfile>();

            // Process each node of the config xml
            var sites = from site in configXML.Descendants("Site") where (site.Attribute("path") != null && site.Attribute("path").Value != "") select site;
            foreach (XElement site in sites)
            {
                // Load each Site node
                Site.SiteProfile siteProfile = new Site.SiteProfile();
                siteProfiles.Add(siteProfile);

                #region Load site profile attributes
                siteProfile.RootDirectoryInfo = new DirectoryInfo(site.Attribute("path").Value);
                #endregion
                #region Error check loaded values
                // Nothing required
                #endregion

                var directories = from directory in sites.Descendants("Directory") select directory;
                foreach (XElement directory in directories)
                {
                    // Load each directory node
                    Site.SiteDirectory siteDirectory = new Site.SiteDirectory();
                    siteProfile.Directories.Add(siteDirectory);

                    #region Load directory attributes
                    siteDirectory.Tool = processor.ProcessorTools[directory.Attribute("processorTool").ValueOrDefault(processor.ProcessorTools.ElementAt(0).Value.Name)];
                    siteDirectory.RelativePath = directory.Attribute("relativePath").ValueOrDefault(DEFAULT_DIRECTORY_RELATIVEPATH);
                    siteDirectory.DirectoryInfo = new DirectoryInfo(RelativizePath(siteProfile.RootDirectoryInfo.FullName, siteDirectory.RelativePath));
                    siteDirectory.FileExtensions = directory.Attribute("filePattern").ValueOrDefault(DEFAULT_DIRECTORY_FILEPATTERN, FILE_PATTERN_DELIMITER);
                    #endregion
                    #region Error check loaded values
                    // Nothing required
                    #endregion

                    #region Plugin loading
                    // Load plugin ProcessorToolDirective calls
                    var preProcessorToolDirectives = from pre in directory.Element("ProcessorToolDirectives").Element("Pre").Elements("Directive")
                                                     select pre.Attribute("name").Value;
                    var postProcessorToolDirectives = from post in directory.Element("ProcessorToolDirectives").Element("Post").Elements("Directive")
                                                     select post.Attribute("name").Value;

                    foreach (string name in preProcessorToolDirectives)
                    {
                        if (processor.ProcessorToolDirectives.ContainsKey(name))
                        {
                            siteDirectory.PreProcessorDirectives.Add(processor.ProcessorToolDirectives[name]);
                        }
                    }
                    foreach (string name in preProcessorToolDirectives)
                    {
                        if (processor.ProcessorToolDirectives.ContainsKey(name))
                        {
                            siteDirectory.PostProcessorDirectives.Add(processor.ProcessorToolDirectives[name]);
                        }
                    }
                    #endregion Plugin loading

                    var exclusions = from exclusion in directory.Descendants("Exclusions").Descendants("Exclusion") select exclusion;
                    foreach (XElement exclusion in exclusions)
                    {
                        // Load each exclusion
                        Site.Exclusion siteExclusion = new Site.Exclusion();
                        siteDirectory.Exclusions.Add(siteExclusion);

                        #region Load exclusion attributes
                        siteExclusion.Type = (Enums.ExclusionType) exclusion.Attribute("type").ValueOrDefault(DEFAULT_EXCLUSION_TYPE, typeof(Enums.ExclusionType));
                        #endregion
                        #region Error check loaded values
                        // Nothing required
                        #endregion

                        var paths = from path in exclusion.Descendants("Path") select path;
                        foreach (XElement path in paths)
                        {
                            // Load each path for an exclusion
                            Site.SitePath sitePath = new Site.SitePath();
                            siteExclusion.Paths.Add(sitePath);

                            #region Load path attributes
                            sitePath.Scope = (Enums.SitePathScope)path.Attribute("scope").ValueOrDefault(DEFAULT_PATH_SCOPE, typeof(Enums.SitePathScope));
                            sitePath.FileInfo = new FileInfo(RelativizePath(siteDirectory.DirectoryInfo.FullName, path.Value));
                            #endregion
                            #region Error check loaded values
                            // Nothing required
                            #endregion
                        }
                    }

                    // TODO: Convert to Plugin?
                    // How would I do that?
                    // Could be a preprocessor that reads in it's rule set, and concats files, modifies the file list.
                    // Could look at @imports in certain files?
                    /**
                    var concatenations = from concatenation in directory.Descendants("Concatenations").Descendants("Concatenation") select concatenation;
                    foreach (XElement concatenation in concatenations)
                    {
                        // Load each concatenation
                        Site.Concatenation siteConcatenation = new Site.Concatenation();
                        siteDirectory.Concatenations.Add(siteConcatenation);

                        #region Load concatenation attributes
                        siteConcatenation.DeleteSourceFiles = concatenation.Attribute("deleteSourceFiles").ValueOrDefault(DEFAULT_CONCAT_DELETESOURCEFILES);
                        siteConcatenation.OutputFilePath = new FileInfo( RelativizePath(siteProfile.RootDirectoryInfo.Parent.Name, concatenation.Attribute("output").ValueOrDefault(DEFAULT_CONCAT_OUTPUT) ) );
                        #endregion
                        #region Error check loaded values
                        // Nothing required
                        #endregion

                        var paths = from path in concatenation.Descendants("Path") select path;
                        foreach (XElement path in paths)
                        {
                            // Load each path for a concatenation
                            Site.SitePath sitePath = new Site.SitePath();
                            siteConcatenation.Paths.Add(sitePath);

                            #region Load path attributes
                            sitePath.FileInfo = new FileInfo(RelativizePath(siteDirectory.DirectoryInfo.FullName, path.Value));
                            #endregion
                            #region Error check loaded values
                            // Nothing required
                            #endregion
                        }
                    }
                    **/
                }
            }

            return siteProfiles;
        }

        /// <summary>
        /// Gets a collection of attribute KVP's for the plugin
        /// </summary>
        /// <param name="node"></param>
        /// <returns></returns>
        private static Dictionary<string, string> GetPluginAttributes(XElement node)
        {
            List<string> ignoredAttributeStrings = new List<string>(new string[3]{ "name", "type", "path" });

            Dictionary<string, string> attributes = new Dictionary<string,string>();

            foreach (XAttribute attr in node.Attributes())
            {
                if (!ignoredAttributeStrings.Contains(attr.Name.ToString()))
                {
                    attributes.Add(attr.Name.ToString(), attr.Value.ToString());
                }
            }

            return attributes;
        }

        #endregion

        #region File path helpers

        /// <summary>
        /// Converts a path string to a relative path string. It replaces the tilder with the current execution directory.
        /// i.e. ~/file.exe --> C:\temp\file.exe
        /// </summary>
        /// <param name="path">Path to relativize</param>
        /// <returns>Full file system path. i.e. ~/file.exe --> C:\temp\file.exe</returns>
        private static string RelativizePath(string path)
        {
            if (path.StartsWith(@"/") || path.StartsWith(@"\"))
            {
                path = "~" + path;
            }

            if (path.StartsWith(@"~"))
            {
                string currentDirectory = System.IO.Directory.GetCurrentDirectory();
                path = CombinePaths(currentDirectory, path.Substring(1));
            }

            return path;
        }

        /// <summary>
        /// Converts a path string to a relative path string. It replaces the tilder with the root string variable
        /// i.e. ~/file.exe --> C:\temp\file.exe
        /// </summary>
        /// <param name="root">Root path</param>
        /// <param name="path">Path to relativize</param>
        /// <returns>Full file system path. i.e. ~\file.exe --> {root variable}\file.exe</returns>
        private static string RelativizePath(string root, string path)
        {
            if (path.StartsWith(@"/") || path.StartsWith(@"\"))
            {
                path = @"~" + path;
            }
            else if (path.StartsWith(@"~"))
            {
                path = path.Substring(1);
            }
            else
            {
                path = @"\" + path;
            }

            path = CombinePaths(root, path);

            return path;
        }

        /// <summary>
        /// A wrapper for System.IO.Path.Combine that makes it a bit more robust to preceeding slashes
        /// </summary>
        /// <param name="path1"></param>
        /// <param name="path2"></param>
        /// <returns>path1 combined with path2</returns>
        private static string CombinePaths(string path1, string path2)
        {
            if (path2[0] == '/' || path2[0] == '\\')
            {
                path2 = path2.Substring(1);
            }

            return System.IO.Path.Combine(path1, path2);
        }

        #endregion
    }
}
